package top.rainbowecho.ai.domain.model.robot;

import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.baidu.aip.contentcensor.AipContentCensor;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Stopwatch;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.rainbowecho.ai.config.RobotConfiguration;
import top.rainbowecho.ai.config.TencentCloudConfiguration;
import top.rainbowecho.ai.domain.model.robot.strategy.SensitiveImageRecognizeStrategy;
import top.rainbowecho.ai.domain.model.robot.strategy.SensitiveTextRecognizeStrategy;
import top.rainbowecho.ai.domain.model.robot.value.AiReply;
import top.rainbowecho.ai.domain.model.robot.value.image.SensitiveImageReply;
import top.rainbowecho.ai.domain.model.robot.value.talk.Talk;
import top.rainbowecho.ai.domain.model.robot.value.talk.TalkAnswer;
import top.rainbowecho.ai.domain.model.robot.value.text.BaiduCheckResult;
import top.rainbowecho.common.aienum.AiParamKey;
import top.rainbowecho.common.domain.shared.vo.SensitiveRecognizeResult;
import top.rainbowecho.common.exception.AiPostException;
import top.rainbowecho.common.util.ExceptionMessageContent;
import top.rainbowecho.common.util.UUIDUtil;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * @author rainbow
 */
@Component
@Slf4j
public class AiRobot implements IAiRobot {
    private RobotConfiguration robotConfiguration;

    private ObjectMapper objectMapper;

    /**
     * 色情图片鉴别策略bean的名字
     */
    public static final String PORN_STRATEGY = "pornStrategy";

    /**
     * 暴恐图片鉴别策略bean的名字
     */
    public static final String TERRORISM_STRATEGY = "terrorismStrategy";

    /**
     * 使用百度AI进行文本审核，因为腾讯相关接口不稳定
     */
    private AipContentCensor baiduAiClient;

    private Map<String, SensitiveImageRecognizeStrategy> imageRecognizeStrategyMap;

    private SensitiveTextRecognizeStrategy textRecognizeStrategy;

    @Autowired
    public void setTextRecognizeStrategy(SensitiveTextRecognizeStrategy textRecognizeStrategy) {
        this.textRecognizeStrategy = textRecognizeStrategy;
    }

    @Autowired
    public void setImageRecognizeStrategyMap(Map<String, SensitiveImageRecognizeStrategy> imageRecognizeStrategyMap) {
        this.imageRecognizeStrategyMap = imageRecognizeStrategyMap;
    }

    @Autowired
    public void setBaiduAiClient(AipContentCensor baiduAiClient) {
        this.baiduAiClient = baiduAiClient;
    }

    @Autowired
    public void setObjectMapper(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Autowired
    public void setRobotConfiguration(RobotConfiguration robotConfiguration) {
        this.robotConfiguration = robotConfiguration;
    }

    /**
     * 根据URL，向AI平台接口发起POST请求
     *
     * @param url    不同AI能力所对应的URL
     * @param params 请求URL所需要的参数Map
     * @return 如果请求成功返回HTTP响应报文的响应体，反之抛出异常
     */
    private String doHttpPost(String url, Map<String, Object> params) {
        HttpResponse response = HttpRequest.post(url)
                .form(params)
                .timeout(5000)
                .execute();
        if (response.isOk()) {
            return response.body();
        }
        throw new AiPostException(ExceptionMessageContent.AI_POST_ERROR);
    }

    /**
     * AI闲聊能力
     *
     * @param talk 用户会话对象
     * @return 机器人应答
     * @throws IOException 反序列化失败抛出异常
     */
    @Override
    public AiReply<TalkAnswer> talk(Talk talk) throws IOException, InterruptedException, ExecutionException, TimeoutException {
        TreeMap<String, String> map = new TreeMap<>();
        map.put(AiParamKey.SESSION.toString(), talk.getSession());
        map.put(AiParamKey.QUESTION.toString(), talk.getQuestion());

        String talkUrl = robotConfiguration.getTencent().getAbility().getTalk().getUrl();
        String json = asyncReq(talkUrl, map).get(1, TimeUnit.SECONDS);
        return objectMapper.readValue(json, new TypeReference<AiReply<TalkAnswer>>() {
        });
    }

    /**
     * 以异步方式请求AI接口，并以string类型接收返回请求接口获得的数据
     *
     * @param url AI api接口
     * @param map 接口所需参数
     * @return string格式的响应
     */
    private CompletableFuture<String> asyncReq(String url, TreeMap<String, String> map) {
        String sign = this.calcSign(map);
        map.put(AiParamKey.SIGN.toString(), sign);
        TreeMap<String, Object> param = new TreeMap<>(map);
        return CompletableFuture.supplyAsync(() -> doHttpPost(url, param));
    }

    /**
     * 计算接口请求所需的签名
     *
     * @param map 接口请求参数map
     * @return 签名
     */
    private String calcSign(TreeMap<String, String> map) {
        StringBuilder sb = new StringBuilder();
        // 秒级的当前时间
        String timestamp = String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));

        // 添加所需的固定参数
        map.put(AiParamKey.APP_ID.toString(), String.valueOf(robotConfiguration.getTencent().getAppId()));
        map.put(AiParamKey.TIMESTAMP.toString(), timestamp);
        map.put(AiParamKey.NONCE_STR.toString(), UUIDUtil.randomUuid());

        for (Map.Entry<String, String> entry : map.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            if (StringUtils.equals(AiParamKey.SIGN.toString(), key) || StringUtils.equals(AiParamKey.APP_KEY.toString(), key)) {
                continue;
            }
            // 对参数值进行URL编码
            String urlEncode = null;
            try {
                urlEncode = URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
            } catch (UnsupportedEncodingException e) {
                log.error(e.getMessage());
            }
            sb.append(String.format("%s=%s&", key, urlEncode));
        }
        sb.append(String.format("%s=%s", AiParamKey.APP_KEY.toString(), robotConfiguration.getTencent().getAppKey()));
        // 对明文签名进行md5加密
        return SecureUtil.md5(sb.toString()).toUpperCase();
    }

    /**
     * 提供图片暴恐，图片鉴黄能力
     *
     * @param imageCode 图片的base64编码
     * @return 识别结果
     * @throws IOException json反序列化失败
     */
    @Override
    public SensitiveRecognizeResult sensitiveImageCheck(String imageCode) throws IOException, InterruptedException, ExecutionException, TimeoutException {
        TreeMap<String, String> map = new TreeMap<>();
        // 将图片base64编码放入参数map中
        map.put(AiParamKey.IMAGE.toString(), imageCode);

        // 根据类型设置对应的AI能力请求URL，不使用流进行操作。流操作会增加新的线程进行处理，造成不必要的线程上下文切换，从而降低效率
        TencentCloudConfiguration.Ability tencentAiInterfaces = robotConfiguration.getTencent().getAbility();

        // 进行图片检查，并反序列化检查结果
        String pornUrl = tencentAiInterfaces.getPorn().getUrl();
        Stopwatch pornRecognize = Stopwatch.createStarted();
        String pornJson = asyncReq(pornUrl, map).get(2, TimeUnit.SECONDS);
        long pornRecognizeDuration = pornRecognize.elapsed(TimeUnit.MILLISECONDS);
        log.info("recognize porn use: " + pornRecognizeDuration);

        AiReply<SensitiveImageReply> aiReply = objectMapper.readValue(pornJson, new TypeReference<AiReply<SensitiveImageReply>>() {});

        SensitiveRecognizeResult pornRecognizeResult = imageRecognizeStrategyMap.get(PORN_STRATEGY)
                .recognize(aiReply.getData().getTag_list());

        if (!pornRecognizeResult.isSensitive()) {
            // 如果不属于色情图片，再进行暴恐图片识别，这样可以避免一次HTTP请求，提高效率
            String terrorismUrl = tencentAiInterfaces.getTerrorism().getUrl();

            Stopwatch terrorismRecognize = Stopwatch.createStarted();
            String terrorismJson = asyncReq(terrorismUrl, map).get(2, TimeUnit.SECONDS);
            long terrorismRecognizeDuration = terrorismRecognize.elapsed(TimeUnit.MILLISECONDS);
            log.info("recognize terrorism use: " + terrorismRecognizeDuration);

            AiReply<SensitiveImageReply> reply = objectMapper.readValue(terrorismJson, new TypeReference<AiReply<SensitiveImageReply>>() {
            });

            return imageRecognizeStrategyMap.get(TERRORISM_STRATEGY).recognize(reply.getData().getTag_list());

        } else {
            return pornRecognizeResult;
        }
    }

    /**
     * 使用百度云文本审核接口
     *
     * @param text 审核文本
     * @return 审核结果
     */
    @Override
    public SensitiveRecognizeResult textCheck(String text) throws IOException {
        String result = baiduAiClient.antiSpam(text, null).toString();

        BaiduCheckResult checkResult = objectMapper.readValue(result, BaiduCheckResult.class);

        return textRecognizeStrategy.recognize(checkResult);
    }
}
